﻿namespace CookieTerm
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Text;
    using Gtk;
    using Cairo;

    /// <summary>
    /// Gtk# implementation of the terminal console.
    /// </summary>
    public class Console : EventBox, IWindow
    {
        static Dictionary<Gdk.Key, char> commandKeyMappings = new Dictionary<Gdk.Key, char>
        {
            {Gdk.Key.BackSpace, '\b'},
            {Gdk.Key.Return, '\r'}
        };

        static Dictionary<Gdk.Key, CookieTerm.Key> keyMappings = new Dictionary<Gdk.Key, Key>
		{
			{Gdk.Key.Up, Key.Up},
			{Gdk.Key.Down, Key.Down},
			{Gdk.Key.Right, Key.Right},
			{Gdk.Key.Left, Key.Left},
			{Gdk.Key.Insert, Key.Insert},
			{Gdk.Key.Delete, Key.Delete},
			{Gdk.Key.Home, Key.Home},
			{Gdk.Key.End, Key.End},
			{Gdk.Key.Prior, Key.Prior},
			{Gdk.Key.Next, Key.Next},
			{Gdk.Key.Tab, Key.Tab}
		};

        Bookmark bookmark;
        Controller controller;
        Tab tab;

        EventHandler<ConsoleInputReceivedEventArgs> onConsoleInputReceived;
        string statusText = string.Empty;
        bool blinkingVisible = true;
        // Counting the number of seconds the user has been idling.
        int idleCounter;
        object idleCounterLock = new object();

        #region Native drawing resources.
        int charWidth;
        int charHeight;
        #endregion

        // Text selection related variables
        bool mouseDragging;
        Nullable<Gdk.Point> draggingStart;
        SelectionManager selectionManager;

        public event EventHandler OnStatusChanged;

        public Controller Controller
        {
            get
            {
                return this.controller;
            }
        }

        int Rows
        {
            get
            {
                return this.bookmark.DisplaySettings.Rows;
            }
        }

        int Columns
        {
            get
            {
                return this.bookmark.DisplaySettings.Columns;
            }
        }

        public Console(Bookmark bookmark, Tab tab)
        {
            this.bookmark = bookmark;
            this.tab = tab;

            XmlColor black = this.bookmark.DisplaySettings.Black;
            ModifyBg(StateType.Normal, new Gdk.Color(black.R, black.G, black.B));
            CanFocus = true;

            // Create a controller
            this.controller = new Controller(bookmark, this);
            this.selectionManager = new SelectionManager(this, this.controller);
        }

        public void Close()
        {
            DisposeDrawingResources();
            this.controller.ShutDown();
        }

        /// <summary>
        /// Refresh the look of the console, based on new display settings. Note that the rows anc columns 
        /// of the console will not be updated. They are fixed when the console is constructed.
        /// </summary>
        public void RefreshDisplaySettings()
        {
            // Measure the size of the character
            int width, height;
            Pango.Layout layout = new Pango.Layout(this.PangoContext);
            layout.FontDescription = new Pango.FontDescription
            {
                Family = this.bookmark.DisplaySettings.FontName,
                Size = (int)(Pango.Scale.PangoScale * this.bookmark.DisplaySettings.FontSize),
                Weight = Pango.Weight.Normal
            };
            layout.SetText("汉");
            layout.GetPixelSize(out width, out height);
            this.charWidth = (int)Math.Ceiling(width / 2f);
            this.charHeight = height;

            // Resize the console
            int w = this.charWidth * Columns;
            int h = this.charHeight * Rows;
            SetSizeRequest(w, h);

            // Redraw the console
            XmlColor black = this.bookmark.DisplaySettings.Black;
            QueueDraw();
        }

        public void Copy()
        {
            string selection = this.selectionManager.GetSelectedText(true);
            if (!string.IsNullOrEmpty(selection))
            {
                Clipboard clipboard = GetClipboard(Gdk.Selection.Clipboard);
                clipboard.Text = selection;
            }
        }

        public void Paste()
        {
            Clipboard clipboard = GetClipboard(Gdk.Selection.Clipboard);
            clipboard.RequestText(PasteReceived);
        }

        public void PasteReceived(Clipboard clipboard, string text)
        {
            if (!string.IsNullOrEmpty(text) && this.OnConsoleInputReceived != null)
            {
                byte[] input = this.bookmark.DisplaySettings.Encoding.GetBytes(text);
                this.OnConsoleInputReceived(this, new ConsoleInputReceivedEventArgs(input));
            }
        }

        public void SelectAll()
        {
            this.selectionManager.SelectAll();
        }

        public void LaunchSelectionAsUrl()
        {
            string s = this.selectionManager.GetSelectedText(true);
            if (!string.IsNullOrEmpty(s))
            {
                if (s.StartsWith("http://") || s.StartsWith("https://"))
                {
                    Process.Start(s);
                }
            }
        }

        protected override void OnRealized()
        {
            base.OnRealized();

            this.GdkWindow.Cursor = new Gdk.Cursor(Gdk.CursorType.Xterm);
            this.RefreshDisplaySettings();

            // Set up timer for cursor and character blinking
            GLib.Timeout.Add(this.bookmark.DisplaySettings.BlinkingInterval, OnBlinkingTimer_Tick);

            // Set up timer for anti idle
            if (this.bookmark.DisplaySettings.AntiIdle)
            {
                GLib.Timeout.Add(1000, OnAntiIdleTimerElapsed);
            }

            this.controller.BeginConnect();
        }

        protected override bool OnKeyPressEvent(Gdk.EventKey e)
        {
            if (OnConsoleInputReceived != null)
            {
                byte[] data = null;
                char ch;
                Key key;

                if (keyMappings.TryGetValue(e.Key, out key))
                {
                    // If the terminal settings map the key to a sequence, use it.
                    this.bookmark.TerminalSettings.KeyToSequenceMapping.TryGetValue(key, out data);
                }
                else if (commandKeyMappings.TryGetValue(e.Key, out ch))
                {
                    // Translate tab, return, and backspace. These keys don't have corresponding 
                    // ascii values like \t, \r, and \b.
                    data = new byte[] { (byte)ch };
                }
                else if (e.State.HasFlag(Gdk.ModifierType.ControlMask) && e.Key <= Gdk.Key.z && e.Key >= Gdk.Key.a)
                {
                    // Translate [Ctrl-a..Ctrl-z] to ascii values [1..26]
                    data = new byte[] { (byte)(e.Key - Gdk.Key.a + 1) };
                }
                else if (Controller.IsAscii((char)e.KeyValue))
                {
                    // Ascii keys translate to single bye
                    data = new byte[] { (byte)e.KeyValue };
                }
                else if (e.KeyValue >= 0x10000)
                {
                    // Unicode keys.
                    data = this.bookmark.DisplaySettings.Encoding.GetBytes(new char[] { (char)e.KeyValue });
                }

                if (data != null)
                {
                    OnConsoleInputReceived(this, new ConsoleInputReceivedEventArgs(data));
                    return true;
                }
            }

            return base.OnKeyPressEvent(e);
        }

        protected override bool OnButtonPressEvent(Gdk.EventButton e)
        {
            if (e.Button == 1)
            {
                // 1 : left button
                this.selectionManager.ClearTextSelection();
                this.draggingStart = new Gdk.Point((int)e.X, (int)e.Y);
            }

            if (e.Button == 3)
            {
                // 3: right button
                this.tab.GetMainWindow().PopupContextMenu();
            }
            return true;
        }

        protected override bool OnButtonReleaseEvent(Gdk.EventButton e)
        {
            this.mouseDragging = false;
            this.draggingStart = null;
            return true;
        }

        protected override bool OnMotionNotifyEvent(Gdk.EventMotion e)
        {
            if (!this.mouseDragging && this.draggingStart.HasValue)
            {
                // The user has clicked the button but has not yet moved out out the current character.
                if (Math.Abs(e.X - this.draggingStart.Value.X) + Math.Abs(e.Y - this.draggingStart.Value.Y) > 4)
                {
                    this.mouseDragging = true;
                    this.selectionManager.StartTextSelection(ScreenCoordinateToConsoleCell(this.draggingStart.Value.X, this.draggingStart.Value.Y));
                }
            }

            if (this.mouseDragging)
            {
                // The user is selecting text
                ConsoleCell p = this.ScreenCoordinateToConsoleCell((int)e.X, (int)e.Y);
                this.selectionManager.ExtendTextSelectionTo(p);
            }

            return true;
        }

        protected override bool OnExposeEvent(Gdk.EventExpose e)
        {
            Cairo.Context context = Gdk.CairoHelper.Create(e.Window);
            Pango.Layout layout = new Pango.Layout(this.PangoContext)
            {
                FontDescription = new Pango.FontDescription
                {
                    Family = this.bookmark.DisplaySettings.FontName,
                    Size = (int)(Pango.Scale.PangoScale * this.bookmark.DisplaySettings.FontSize),
                    Weight = Pango.Weight.Normal,
                }
            };

            Gdk.GC gc = new Gdk.GC(this.GdkWindow);

            for (int r = 1; r <= Rows; r++)
            {
                for (int c = 1; c <= Columns; c++)
                {
                    CharAttribute cell = Controller.VideoBuffer[r, c];
                    char ch = cell.Character;

                    if (ch == 0)
                    {
                        continue;
                    }

                    AnsiColor fgColor;
                    AnsiColor bgColor;

                    // Inverse attribute && selection
                    if (cell.Attributes.HasInverse() ^ this.selectionManager.IsSelected(r, c))
                    {
                        fgColor = cell.Background;
                        bgColor = cell.Foreground;
                    }
                    else
                    {
                        fgColor = cell.Foreground;
                        bgColor = cell.Background;
                    }

                    // Underline attribute
                    //Font font = cell.Attributes.HasUnderline() ? this.UnderlineFont : this.NormalFont;

                    // Non ASCII double width character
                    bool doubleWidth = !Controller.IsAscii(ch) && c < Columns;

                    int x = (c - 1) * this.charWidth;
                    int y = (r - 1) * this.charHeight;
                    int w = doubleWidth ? this.charWidth * 2 : this.charWidth;
                    int h = this.charHeight;

                    // Cursor
                    bool isCursor = this.blinkingVisible && Controller.ShowCursor && c == Controller.CursorColumn && r == Controller.CursorRow;

                    // Draw background
                    XmlColor bgXmlColor = isCursor ? this.bookmark.DisplaySettings.CursorColor : this.bookmark.DisplaySettings.Palette[(int)bgColor];
                    context.SetSourceRGB(bgXmlColor.R / 256f, bgXmlColor.G / 256f, bgXmlColor.B / 256f);
                    context.Rectangle(x, y, w, h);
                    context.Fill();

                    // Draw character
                    if (!cell.Attributes.HasBlink() || this.blinkingVisible)
                    {
                        layout.SetText(ch.ToString());
                        XmlColor fgXmlColor = (cell.Attributes.HasBold() ? this.bookmark.DisplaySettings.BoldPalette : this.bookmark.DisplaySettings.Palette)[(int)fgColor];
                        gc.RgbFgColor = new Gdk.Color(fgXmlColor.R, fgXmlColor.G, fgXmlColor.B);
                        this.GdkWindow.DrawLayout(gc, x, y, layout);
                    }
                }
            }

            context.Dispose();
            return true;
        }

        bool OnBlinkingTimer_Tick()
        {
            this.blinkingVisible = !this.blinkingVisible;
            // TODO: Redraw the whole console or just the cursor and blinking cells?
            QueueDraw();
            return true;
        }

        bool OnAntiIdleTimerElapsed()
        {
            int n;
            lock (this.idleCounterLock)
            {
                n = ++this.idleCounter;
            }
            if (n >= CookieTermAppSettings.AntiIdleInterval)
            {
                SimulateKeyPress((char)CookieTermAppSettings.AntiIdleChar);
            }
            return true;
        }

        ConsoleCell ScreenCoordinateToConsoleCell(int x, int y)
        {
            int column = x / this.charWidth + 1;
            int row = y / this.charHeight + 1;

            if (row > Rows)
            {
                row = Rows;
            }

            if (column > Columns)
            {
                column = Columns;
            }

            if (row < 1)
            {
                row = 1;
            }

            if (column < 1)
            {
                column = 1;
            }

            if (column > 1 && Controller.VideoBuffer[row, column].Character == 0)
            {
                column--;
            }
            return new ConsoleCell(Columns, row, column);
        }

        void copyMenuItem_Click(object sender, EventArgs e)
        {
            this.Copy();
        }

        void launchAsURLMenuItem_Click(object sender, EventArgs e)
        {
            this.LaunchSelectionAsUrl();
        }

        void pasteMenuItem_Click(object sender, EventArgs e)
        {
            this.Paste();
        }

        void selectAllMenuItem_Click(object sender, EventArgs e)
        {
            this.selectionManager.SelectAll();
        }

        void DisposeDrawingResources()
        {
            // TODO: Not sure what native resources we need to dispose in GTK
        }

        void SimulateKeyPress(char c)
        {
            if (OnConsoleInputReceived != null)
            {
                byte[] data = new byte[] { (byte)c };
                OnConsoleInputReceived(this, new ConsoleInputReceivedEventArgs(data));
            }
        }

        #region IWindow implementation
        // TODO: Not all of them are called from the UI thread. Both GTK and WinForm are not thread safe.
        public EventHandler<ConsoleInputReceivedEventArgs> OnConsoleInputReceived
        {
            get
            {
                return this.onConsoleInputReceived;
            }
            set
            {
                this.onConsoleInputReceived = value;
            }
        }

        public string StatusText
        {
            // We don't have a status bar. It's up to our container to display the status text.
            get
            {
                return this.statusText;
            }
            set
            {
                this.statusText = this.bookmark.Name + " : " + value;
                if (OnStatusChanged != null)
                {
                    Application.Invoke(delegate
                    {
                        if (OnStatusChanged != null)
                        {
                            OnStatusChanged(this, new EventArgs());
                        }
                    });
                }
            }
        }

        public bool PromptForUserNamePassword(out string username, out string password)
        {
			AuthenticationDialog dialog = new AuthenticationDialog ();
            dialog.Parent = this;
			ResponseType response = (ResponseType) dialog.Run ();
            username = dialog.Username;
            password = dialog.Password;
			dialog.Destroy();
			return (response == ResponseType.Ok);
        }

        public void ShowMessageBox(string message, EventHandler continuation)
        {
            Application.Invoke(delegate
            {
                MessageDialog dialog = new MessageDialog(this.tab.GetMainWindow(), DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, message);
                dialog.Run();
                dialog.Destroy();
                continuation(this, new EventArgs());
            });
        }

        public void StartClosing()
        {
            Application.Invoke(delegate
            {
                this.tab.Close();
            });
        }

        public void ResetAntiIdle()
        {
            lock (this.idleCounterLock)
            {
                this.idleCounter = 0;
            }
        }

        public void InvalidateCell(int row, int col)
        {
            InvalidateRect(row, col, row, col);
        }

        public void InvalidateRect(int top, int left, int bottom, int right)
        {
            int l = (left - 1) * this.charWidth;
            int t = (top - 1) * this.charHeight;
            int w = this.charHeight * (right - left + 1);
            int h = this.charHeight * (bottom - top + 1);
            Application.Invoke(delegate
            {
                this.QueueDrawArea(l, t, w, h);
            });
        }
        #endregion
    }
}

